/*
 * Copyright 2000-2006 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jetbrains.idea.accurev.application;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ProjectComponent;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.JDOMExternalizable;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.ColoredTreeCellRenderer;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.util.containers.BidirectionalMap;
import com.intellij.util.containers.FactoryMap;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.accurev.accurev.*;
import org.jetbrains.idea.accurev.accurev.connections.P4Connection;
import org.jetbrains.idea.accurev.operations.VcsOperationLog;
import org.jetbrains.idea.accurev.operations.P4MoveToChangeListOperation;

import java.io.File;
import java.util.*;

/**
 * @author yole
 */
@SuppressWarnings({"UnnecessaryFullyQualifiedName"})
public class AccuRevChangeListSynchronizer implements ProjectComponent, JDOMExternalizable, ChangeListListener, ChangeListDecorator {
  private static final Logger LOG = Logger.getInstance("#org.jetbrains.idea.accurev.application.AccuRevChangeListSynchronizer");
  @NonNls private static final String ELEMENT_CONNECTION = "connection";
  @NonNls private static final String ATTRIBUTE_SERVER = "server";
  @NonNls private static final String ATTRIBUTE_CLIENT = "client";
  @NonNls private static final String ATTRIBUTE_USER = "user";
  @NonNls private static final String ELEMENT_CHANGELIST = "changelist";
  @NonNls private static final String ATTRIBUTE_NAME = "name";
  @NonNls private static final String ATTRIBUTE_NUMBER = "number";

  public static AccuRevChangeListSynchronizer getInstance(Project project) {
    return project.getComponent(AccuRevChangeListSynchronizer.class);
  }

  private static class ConnectionKey {
    public String server;
    public String client;
    public String user;

    public ConnectionKey(PerforceClient client) {
      this(client.getServerPort(), client.getName(), client.getUserName());
    }

    public ConnectionKey(final String server, final String client, final String user) {
      this.server = server;
      this.client = client;
      this.user = user;
    }

    public boolean equals(final Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      final ConnectionKey that = (ConnectionKey)o;

      if (!client.equals(that.client)) return false;
      if (!server.equals(that.server)) return false;
      if (!user.equals(that.user)) return false;

      return true;
    }

    public int hashCode() {
      int result;
      result = server.hashCode();
      result = 31 * result + client.hashCode();
      result = 31 * result + user.hashCode();
      return result;
    }
  }

  private final Project myProject;
  private AccuRevManager myAccuRevManager;
  private ChangeListManager myChangeListManager;
  private ProjectLevelVcsManager myVcsManager;
  private String myLastCommittedChangelistName;
  private Map<ConnectionKey, BidirectionalMap<String, Long>> myChangeListMap = new HashMap<ConnectionKey, BidirectionalMap<String, Long>>();
  private boolean myUpdatingAfterActivation;
  private boolean mySynchronizingName;
  private boolean myDeleteEmptyChangelistsRequested = false;

  private PerforceActivityListener myActivityListener = new PerforceActivityListener() {
    public void changeListSubmitted(P4Connection connection, long changeListNumber, final long newNumber) {
      if (changeListNumber != -1) {
        processChangeListSubmitted(connection, changeListNumber, newNumber);
      }
    }
  };

  public AccuRevChangeListSynchronizer(final Project project, final ChangeListManager changeListManager,
                                final ProjectLevelVcsManager vcsManager) {
    myProject = project;
    myChangeListManager = changeListManager;
    myVcsManager = vcsManager;
  }

  private void createDefaultChangeList() {
    LocalChangeList result = myChangeListManager.findChangeList(getDefaultChangeListName());
    if (result != null) {
      result.setReadOnly(true);
    }
    else {
      final LocalChangeList changeList = myChangeListManager.addChangeList(getDefaultChangeListName(), "");
      changeList.setReadOnly(true);
    }
  }

  private static String getDefaultChangeListName() {
    return VcsBundle.message("changes.default.changlist.name");
  }

  public void projectOpened() {
  }

  public void projectClosed() {
  }

  public void startListening() {
    if (myAccuRevManager == null) {
      myAccuRevManager = AccuRevManager.getInstance(myProject);
    }
    createDefaultChangeList();
    myChangeListManager.addChangeListListener(this);
    AccuRevSettings.getSettings(myProject).addActivityListener(myActivityListener);
  }

  public void stopListening() {
    myChangeListManager.removeChangeListListener(this);
    AccuRevSettings.getSettings(myProject).removeActivityListener(myActivityListener);
  }

  @NotNull
  @NonNls
  public String getComponentName() {
    return "AccuRevChangeListSynchronizer";
  }

  public void initComponent() {
  }

  public void disposeComponent() {
  }

  public void readExternal(Element element) {
    for(Object connElementObj: element.getChildren(ELEMENT_CONNECTION)) {
      Element connElement = (Element) connElementObj;
      ConnectionKey key = new ConnectionKey(connElement.getAttributeValue(ATTRIBUTE_SERVER),
                                            connElement.getAttributeValue(ATTRIBUTE_CLIENT),
                                            connElement.getAttributeValue(ATTRIBUTE_USER));
      BidirectionalMap<String, Long> map = new BidirectionalMap<String, Long>();
      myChangeListMap.put(key, map);
      for(Object listElementObj: connElement.getChildren(ELEMENT_CHANGELIST)) {
        Element listElement = (Element) listElementObj;
        final String name = listElement.getAttributeValue(ATTRIBUTE_NAME);
        if (!map.containsKey(name)) {
          map.put(name, Long.parseLong(listElement.getAttributeValue(ATTRIBUTE_NUMBER)));
        }
      }
    }
  }

  public void writeExternal(Element element) {
    for(ConnectionKey key: myChangeListMap.keySet()) {
      Element connElement = new Element(ELEMENT_CONNECTION);
      connElement.setAttribute(ATTRIBUTE_SERVER, key.server);
      connElement.setAttribute(ATTRIBUTE_CLIENT, key.client);
      connElement.setAttribute(ATTRIBUTE_USER, key.user);
      element.addContent(connElement);
      Map<String, Long> map = myChangeListMap.get(key);
      for(String name: map.keySet()) {
        Element listElement = new Element(ELEMENT_CHANGELIST);
        listElement.setAttribute(ATTRIBUTE_NAME, name);
        listElement.setAttribute(ATTRIBUTE_NUMBER, map.get(name).toString());
        connElement.addContent(listElement);
      }
    }
  }

  public void changeListAdded(ChangeList list) {
  }

  public void changeListRemoved(final ChangeList list) {
    if (myUpdatingAfterActivation) return;
    if (Comparing.equal(myLastCommittedChangelistName, list.getName())) {
      myLastCommittedChangelistName = null;
    }
    else {
      final String changeListName = list.getName();
      myAccuRevManager.queueUpdateRequest(new Runnable() {
        public void run() {
          handleChangeListDelete(changeListName);
        }
      });
    }
  }

  private void handleChangeListDelete(final String changeListName) {
    final AccuRevSettings settings = AccuRevSettings.getSettings(myProject);
    for(P4Connection connection: settings.getAllConnections()) {
      final PerforceClient perforceClient = myAccuRevManager.getClient(connection);
      if (!isValidClient(perforceClient)) continue;
      Map<String, Long> map = getChangeListMap(perforceClient);
      Long value = map.get(changeListName);
      if (value != null) {
        try {
          final AccuRevRunner runner = AccuRevRunner.getInstance(myProject);
          final List<PerforceChangeList> changeLists = runner.getPendingChangeLists(connection);
          for(PerforceChangeList cl: changeLists) {
            if (cl.getNumber() == value.longValue()) {
              runner.deleteEmptyChangeList(connection, value.longValue());
              break;
            }
          }
        }
        catch (VcsException e) {
          LOG.error(e);
        }
        map.remove(changeListName);
      }
    }
  }

  private static boolean isValidClient(final PerforceClient perforceClient) {
    return perforceClient.getName() != null && perforceClient.getUserName() != null && perforceClient.getServerPort() != null;
  }

  public void changeListChanged(final ChangeList list) {
  }

  private void checkReopenChangesAsync(final Collection<Change> changes, final ChangeList list) {
    myAccuRevManager.queueUpdateRequest(new Runnable() {
      public void run() {
        if (myProject.isDisposed()) return;
        LOG.debug("changeListChanged in event dispatch thread: " + list.getName());
        checkReopenChanges(changes, list);
      }
    });
  }

  public void changeListRenamed(final ChangeList list, final String oldName) {
    if (!mySynchronizingName) {
      myAccuRevManager.queueUpdateRequest(new Runnable() {
        public void run() {
          handleChangeListRename(list, oldName);
        }
      });
    }
  }

  public void changeListCommentChanged(final ChangeList list, final String oldComment) {
    if (!mySynchronizingName) {
      myAccuRevManager.queueUpdateRequest(new Runnable() {
        public void run() {
          handleChangeListRename(list, list.getName());
        }
      });
    }
  }

  private void handleChangeListRename(final ChangeList list, final String oldName) {
    final AccuRevSettings settings = AccuRevSettings.getSettings(myProject);
    Set<ConnectionKey> processedKeys = new HashSet<ConnectionKey>();
    for(P4Connection connection: settings.getAllConnections()) {
      final PerforceClient client = myAccuRevManager.getClient(connection);
      if (!isValidClient(client)) continue;
      ConnectionKey key = new ConnectionKey(client);
      if (processedKeys.contains(key)) continue;
      processedKeys.add(key);

      final BidirectionalMap<String, Long> changeListMap = getChangeListMap(client);
      Long number = changeListMap.get(oldName);
      if (number != null) {
        try {
          AccuRevRunner.getInstance(myProject).renameChangeList(number.longValue(), getP4Description(list), connection);
        }
        catch(VcsException e) {
          LOG.error(e);
        }
        changeListMap.remove(oldName);
        LOG.assertTrue(!changeListMap.containsValue(number));
        changeListMap.put(list.getName(), number);
      }
    }
  }

  private void checkReopenChanges(final Collection<Change> changes, final ChangeList inList) {
    final AccuRevSettings settings = AccuRevSettings.getSettings(myProject);
    Map<ConnectionKey, List<PerforceChange>> opened = new HashMap<ConnectionKey, List<PerforceChange>>();
    Map<Long, List<File>> filesToReopen = new HashMap<Long, List<File>>();
    FactoryMap<P4Connection, PerforceClient> clientMap = new FactoryMap<P4Connection, PerforceClient>() {
      protected PerforceClient create(final P4Connection key) {
        return myAccuRevManager.getClient(key);
      }
    };

    Collection<Change> changesToProcess;
    if (changes == null) {
      changesToProcess = inList.getChanges();
    }
    else {
      changesToProcess = new ArrayList<Change>();
      for(Change c: changes) {
        if (myChangeListManager.getChangeList(c) != null) {
          changesToProcess.add(c);
        }
      }
    }

    if (!settings.ENABLED) {
      for(final Change change: changesToProcess) {
        VcsOperationLog.getInstance(myProject).addToLog(new P4MoveToChangeListOperation(change, inList.getName()));
      }
      return;
    }
    for(final Change change: changesToProcess) {
      AbstractVcs vcs = ApplicationManager.getApplication().runReadAction(new Computable<AbstractVcs>() {
        public AbstractVcs compute() {
          return ChangesUtil.getVcsForChange(change, myProject);
        }
      });
      if (vcs instanceof AccuRevVcs) {
        FilePath afterPath = null;
        ContentRevision afterRevision = change.getAfterRevision();
        if (afterRevision != null) {
          afterPath = afterRevision.getFile();
          checkReopenFile(afterPath, inList, opened, filesToReopen, clientMap);
        }
        ContentRevision beforeRevision = change.getBeforeRevision();
        if (beforeRevision != null) {
          FilePath beforePath = beforeRevision.getFile();
          if (afterPath == null || !beforePath.equals(afterPath)) {
            checkReopenFile(beforePath, inList, opened, filesToReopen, clientMap);
          }
        }
      }
      else {
        LOG.debug("checkReopenChanges(): change not under P4");
      }
    }

    for(final Long changeList: filesToReopen.keySet()) {
      final List<File> files = filesToReopen.get(changeList);
      ApplicationManager.getApplication().runReadAction(new Runnable() {
        public void run() {
          try {
            AccuRevRunner.getInstance(myProject).reopen(files.toArray(new File[files.size()]), changeList.longValue());
          }
          catch (VcsException e) {
            LOG.debug(e);
          }
        }
      });
    }
  }

  private void checkReopenFile(final FilePath filePath, final ChangeList inList, final Map<ConnectionKey, List<PerforceChange>> opened,
                               final Map<Long, List<File>> filesToReopen,
                               final FactoryMap<P4Connection, PerforceClient> clientMap) {
    final P4Connection connection = ApplicationManager.getApplication().runReadAction(new Computable<P4Connection>() {
      public P4Connection compute() {
        return AccuRevSettings.getSettings(myProject).getConnectionForFile(filePath.getIOFile());
      }
    });

    final PerforceClient client = clientMap.get(connection);
    if (!isValidClient(client)) {
      LOG.debug("skipping checkReopenChanges() because client is not valid");
      return;
    }
    ConnectionKey key = new ConnectionKey(client);
    long openedChangeListNumber;
    long changeListNumber;
    try {
      changeListNumber = findOrCreatePerforceChangeList(connection, inList);
      openedChangeListNumber = getOpenedChangeListNumber(filePath.getIOFile(), opened, key, connection, client);
    }
    catch (VcsException e) {
      if (e.getMessage() != null && e.getMessage().length() > 0) {
        LOG.info(e);
      }
      return;
    }
    if (LOG.isDebugEnabled()) {
      LOG.debug("checkReopenChanges() for " + filePath + ": IDEA changelist " + changeListNumber + ", P4 changelist " + openedChangeListNumber);
    }
    if (openedChangeListNumber != changeListNumber) {
      List<File> fileList = filesToReopen.get(changeListNumber);
      if (fileList == null) {
        fileList = new ArrayList<File>();
        filesToReopen.put(changeListNumber, fileList);
      }
      fileList.add(filePath.getIOFile());
    }
  }

  public void changesMoved(Collection<Change> changes, ChangeList fromList, ChangeList toList) {
    if (!Comparing.equal(fromList.getName(), myLastCommittedChangelistName) && !myUpdatingAfterActivation) {
      checkReopenChangesAsync(changes, toList);
    }
  }

  private long getOpenedChangeListNumber(@NotNull final File f, final Map<ConnectionKey, List<PerforceChange>> opened,
                                         final ConnectionKey key, final P4Connection connection,
                                         final PerforceClient client) throws VcsException {
    List<PerforceChange> openedList = opened.get(key);
    if (openedList == null) {
      openedList = AccuRevRunner.getInstance(myProject).opened(connection);
      opened.put(key, openedList);
    }
    final String path = f.getPath().replace('/', File.separatorChar);
    final String lowerName = f.getName().toLowerCase();
    for(PerforceChange openedFile: openedList) {
      // getFileByDepotName() is expensive, so we check for simple filename match before doing the full check
      if (openedFile.getDepotPath().toLowerCase().indexOf(lowerName) < 0) continue;

      File file = AccuRevManager.getFileByDepotName(openedFile.getDepotPath(), client);
      if (file != null && file.getAbsolutePath().equalsIgnoreCase(path)) {
        return openedFile.getChangeList();
      }
    }
    return -1;
  }

  public long findOrCreatePerforceChangeList(final P4Connection connection, final ChangeList list) throws VcsException {
    if (list.getName().equals(getDefaultChangeListName())) {
      return -1;
    }
    final PerforceClient client = myAccuRevManager.getClient(connection);
    if (!isValidClient(client)) {
      return -1;
    }
    BidirectionalMap<String, Long> nameToChangelistMap = getChangeListMap(client);
    if (!nameToChangelistMap.containsKey(list.getName())) {
      return createListInPerforce(list, connection);
    }
    return nameToChangelistMap.get(list.getName()).longValue();
  }

  private long createListInPerforce(final ChangeList list, final P4Connection connection) throws VcsException {
    final PerforceClient client = myAccuRevManager.getClient(connection);
    if (!isValidClient(client)) {
      return -1;
    }
    BidirectionalMap<String, Long> nameToChangelistMap = getChangeListMap(client);

    String description = getP4Description(list);
    long changeListNumber = AccuRevRunner.getInstance(myProject).createChangeList(description, connection, null);
    LOG.assertTrue(!nameToChangelistMap.containsValue(changeListNumber));
    nameToChangelistMap.put(list.getName(), changeListNumber);
    return changeListNumber;
  }

  private static String getP4Description(final ChangeList list) {
    String description = list.getComment();
    if (description.length() == 0) description = list.getName();
    return description;
  }

  @NotNull
  private BidirectionalMap<String, Long> getChangeListMap(final PerforceClient client) {
    ConnectionKey key = new ConnectionKey(client);
    BidirectionalMap<String, Long> nameToChangelistMap = myChangeListMap.get(key);
    if (nameToChangelistMap == null) {
      nameToChangelistMap = new BidirectionalMap<String, Long>();
      myChangeListMap.put(key, nameToChangelistMap);
    }
    return nameToChangelistMap;
  }

  public void defaultListChanged(ChangeList newDefaultList) {
  }

  public void unchangedFileStatusChanged() {
  }

  public void changeListUpdateDone() {
    // if the activation detected that a complete update was required, it will leave myUpdatingAfterActivation as true and
    // queue an async update => we need to reset the flag here
    myUpdatingAfterActivation = false;
    if (myDeleteEmptyChangelistsRequested) {
      myDeleteEmptyChangelistsRequested = true;
      checkDeleteAllEmptyChangelists();
    }
  }

  private void processChangeListSubmitted(final P4Connection connection, final long changeListNumber, final long newNumber) {
    LOG.debug("processChangeListSubmitted: changeListNumber=" + changeListNumber + ", newNumber=" + newNumber);
    String changeListName = findChangeListName(connection, changeListNumber);
    if (changeListName != null) {
      final LocalChangeList changeList = findChangeList(connection, changeListNumber);
      BidirectionalMap<String, Long> changeListMap = getChangeListMap(myAccuRevManager.getClient(connection));
      if (newNumber == -1) {
        changeListMap.remove(changeListName);
        if (changeList == null || !changeList.isDefault()) {
          // the changelist is going to be deleted, and we shouldn't handle subsequent changesMoved notifications as attempts to move changes
          myLastCommittedChangelistName = changeListName;
        }
      }
      else {
        LOG.debug("updated changeListMap");
        LOG.assertTrue(!changeListMap.containsValue(newNumber));
        changeListMap.put(changeListName, newNumber);
      }
    }
    else {
      LOG.debug("Failed to find submitted changelist by name");
    }
  }

  @Nullable
  private String findChangeListName(final P4Connection connection, final long changeListNumber) {
    final PerforceClient perforceClient = myAccuRevManager.getClient(connection);
    if (!isValidClient(perforceClient)) {
      return null;
    }
    BidirectionalMap<String, Long> changeListMap = getChangeListMap(perforceClient);
    String changeListName = null;
    final List<String> list = changeListMap.getKeysByValue(changeListNumber);
    if (list != null) {
      LOG.assertTrue(list.size() <= 1);
      if (list.size() > 0) {
        changeListName = list.get(0);
      }
    }
    return changeListName;
  }

  @Nullable
  public LocalChangeList findChangeList(final P4Connection connection, final long changeListNumber) {
    if (changeListNumber == -1) {
      return myChangeListManager.findChangeList(getDefaultChangeListName());
    }
    String name = findChangeListName(connection, changeListNumber);
    if (name == null) {
      LOG.debug("Couldn't find name for changelist with number " + changeListNumber);
      return null;
    }
    return myChangeListManager.findChangeList(name);
  }

  public ChangeList findOrCreateChangeList(final P4Connection connection, final long changeListNumber, final String changeListDescription) {
    ChangeList result = findChangeList(connection, changeListNumber);
    if (result == null) {
      result = createChangeListByDescription(connection, changeListDescription, changeListNumber);
    }
    return result;
  }

  public ChangeList createChangeListByDescription(final P4Connection connection,
                                                  String changeListDescription,
                                                  final long changeListNumber) {
    final PerforceClient perforceClient = myAccuRevManager.getClient(connection);
    if (!isValidClient(perforceClient)) {
      return myChangeListManager.getDefaultChangeList();
    }
    ConnectionKey key = new ConnectionKey(perforceClient);
    BidirectionalMap<String, Long> changeListMap = myChangeListMap.get(key);

    String name;
    String comment;
    changeListDescription = changeListDescription.trim();
    int pos = changeListDescription.indexOf("\n");
    if (pos >= 0) {
      name = changeListDescription.substring(0, pos).trim() + "...";
      comment = changeListDescription;
    }
    else {
      name = changeListDescription;
      comment = "";
    }
    LocalChangeList result = myChangeListManager.findChangeList(name);
    if (result == null) {
      result = myChangeListManager.addChangeList(name, comment);
    }
    changeListMap.put(result.getName(), changeListNumber);
    return result;
  }

  public void queueUpdateOpenedFiles() {
    myAccuRevManager.queueUpdateRequest(new Runnable() {
      public void run() {
        updateOpenedFiles();
      }
    });
  }

  /**
   * Runs the 'p4 opened' command to see if any updates to the changes tree are required.
   *
   * @return true if all changes have been processed, false if some changes cannot be processed and a complete
   * Changes view update is required.
   */
  public boolean updateOpenedFiles() {
    if (myProject.isDisposed()) return true;
    myUpdatingAfterActivation = true;
    final AccuRevSettings settings = AccuRevSettings.getSettings(myProject);
    if (myAccuRevManager == null) {
      myAccuRevManager = AccuRevManager.getInstance(myProject);
    }
    Set<ConnectionKey> processedKeys = new HashSet<ConnectionKey>();
    for(P4Connection connection: settings.getAllConnections()) {
      final PerforceClient client = myAccuRevManager.getClient(connection);
      if (!isValidClient(client)) continue;
      ConnectionKey key = new ConnectionKey(client);
      if (processedKeys.contains(key)) continue;
      processedKeys.add(key);

      final List<PerforceChangeList> pendingChangeLists;
      try {
        pendingChangeLists = AccuRevRunner.getInstance(myProject).getPendingChangeLists(connection);
      }
      catch (VcsException e) {
        LOG.info(e);
        continue;
      }

      final List<PerforceChange> opened;
      try {
        opened = AccuRevRunner.getInstance(myProject).opened(connection);
      }
      catch (VcsException e) {
        if (e.getMessage() != null && e.getMessage().length() > 0) {
          LOG.info(e);
        }
        continue;
      }
      for(PerforceChange openedFile: opened) {
        if (!updateOpenedFile(connection, client, openedFile, pendingChangeLists)) return false;
      }

      checkDeleteEmptyChangeLists(pendingChangeLists, key);
    }
    LOG.debug("updateOpenedFiles(): All changes matched successfully");
    myUpdatingAfterActivation = false;
    return true;
  }

  public void requestDeleteEmptyChangeLists() {
    myDeleteEmptyChangelistsRequested = true;
  }

  private void checkDeleteAllEmptyChangelists() {
    final AccuRevSettings settings = AccuRevSettings.getSettings(myProject);
    for(P4Connection connection: settings.getAllConnections()) {
      final PerforceClient client = myAccuRevManager.getClient(connection);
      if (!isValidClient(client)) continue;
      ConnectionKey key = new ConnectionKey(client);

      final List<PerforceChangeList> pendingChangeLists;
      try {
        pendingChangeLists = AccuRevRunner.getInstance(myProject).getPendingChangeLists(connection);
      }
      catch (VcsException e) {
        LOG.info(e);
        continue;
      }

      checkDeleteEmptyChangeLists(pendingChangeLists, key);
    }
  }

  private void checkDeleteEmptyChangeLists(final List<PerforceChangeList> pendingChangeLists, final ConnectionKey key) {
    BidirectionalMap<String, Long> nameToChangelistMap = myChangeListMap.get(key);
    if (nameToChangelistMap != null) {
      Set<String> names = new HashSet<String>(nameToChangelistMap.keySet());
      for(String name: names) {
        final LocalChangeList list = myChangeListManager.findChangeList(name);
        final Long number = nameToChangelistMap.get(name);
        if (list != null && number != null && !isPendingChangeList(number.longValue(), pendingChangeLists)) {
          removeChangeListIfEmpty(list);
          nameToChangelistMap.remove(name);
        }
      }
    }
  }

  private void removeChangeListIfEmpty(final LocalChangeList list) {
    if (list.isDefault() || list.isReadOnly()) {
      return;
    }
    for(final Change c: list.getChanges()) {
      final AbstractVcs vcsForChange = ApplicationManager.getApplication().runReadAction(new Computable<AbstractVcs>() {
        public AbstractVcs compute() {
          return ChangesUtil.getVcsForChange(c, myProject);
        }
      });
      if (!(vcsForChange instanceof AccuRevVcs)) {
        // changelist still contains non-P4 changes - keep
        return;
      }
    }
    myChangeListManager.removeChangeList(list);
  }

  private static boolean isPendingChangeList(final long number, final List<PerforceChangeList> list) {
    for(PerforceChangeList cl: list) {
      if (cl.getNumber() == number) return true;
    }
    return false;
  }

  private boolean updateOpenedFile(final P4Connection connection, final PerforceClient client,
                                   final PerforceChange fileChange, final List<PerforceChangeList> pendingChangeLists) {
    final File file;
    try {
      file = AccuRevManager.getFileByDepotName(P4File.unescapeWildcards(fileChange.getDepotPath()), client);
    }
    catch (VcsException e) {
      LOG.info(e);
      return false;
    }

    if (file == null) {//cannot find local mapping for depot path
      return false;
    }

    // ignore changes for files not under content roots
    boolean isUnderProjectVcs = ApplicationManager.getApplication().runReadAction(new Computable<Boolean>() {
      public Boolean compute() {
        VirtualFile parentFile = LocalFileSystem.getInstance().findFileByIoFile(file.getParentFile());
        if (parentFile == null || myVcsManager.getVcsFor(parentFile) != AccuRevVcs.getInstance(myProject)) {
          return false;
        }
        return true;
      }
    }).booleanValue();
    if (!isUnderProjectVcs) return true;

    LocalChangeList changeList = findChangeList(connection, fileChange.getChangeList());
    if (changeList == null) {
      if (LOG.isDebugEnabled()) {
        LOG.debug("updateOpenedFile(): could not find changelist for number " + fileChange.getChangeList());
      }
      return false;
    }
    if (fileChange.getChangeList() != -1) {
      synchronizeChangeListName(client, changeList, fileChange.getChangeList(), pendingChangeLists);
    }
    for(Change change: changeList.getChanges()) {
      if (change.affectsFile(file)) {
        return true;
      }
    }

    List<LocalChangeList> oldChangeLists = myChangeListManager.getChangeLists();
    for(LocalChangeList oldChangeList: oldChangeLists) {
      if (oldChangeList == changeList) continue;
      for(Change change: oldChangeList.getChanges()) {
        if (change.affectsFile(file)) {
          myChangeListManager.moveChangesTo(changeList, new Change[] { change });
          return true;
        }
      }
    }

    if (LOG.isDebugEnabled()) {
      LOG.debug("updateOpenedFile(): could not find changelist for change " + fileChange);
    }
    return false;
  }

  private void synchronizeChangeListName(final PerforceClient client, final LocalChangeList changeList, long number, final List<PerforceChangeList> pendingChangeLists) {
    for(PerforceChangeList perforceChangeList: pendingChangeLists) {
      if (perforceChangeList.getNumber() == number) {
        String description = perforceChangeList.getComment();
        if (!Comparing.equal(changeList.getName(), description) && !Comparing.equal(changeList.getComment(), description)) {
          String oldName = changeList.getName();
          mySynchronizingName = true;
          try {
            description = description.trim();
            String firstLine = getFirstLine(description);

            if (changeList.getComment().length() == 0) {
              if (firstLine != null) {
                changeList.setName(firstLine);
                changeList.setComment(description);
              }
              else {
                changeList.setName(description);
              }
            }
            else if (changeList.getComment() != null && Comparing.equal(changeList.getName(), getFirstLine(changeList.getComment()))) {
              changeList.setName(firstLine);
              changeList.setComment(description);
            }
            else {
              changeList.setComment(description);
            }
          }
          finally {
            mySynchronizingName = false;
          }

          final BidirectionalMap<String, Long> changeListMap = getChangeListMap(client);
          changeListMap.remove(oldName);
          LOG.assertTrue(!changeListMap.containsValue(number));
          changeListMap.put(changeList.getName(), number);
        }
        break;
      }
    }
  }

  @Nullable
  private static String getFirstLine(final String description) {
    String firstLine = null;
    int pos = description.indexOf("\n");
    if (pos >= 0) {
      firstLine = description.substring(0, pos).trim() + "...";
    }
    return firstLine;
  }

  public void decorateChangeList(LocalChangeList changeList, ColoredTreeCellRenderer cellRenderer, boolean selected, boolean expanded,
                                 boolean hasFocus) {
    if (changeList.getName().equals(getDefaultChangeListName())) return;
    for(BidirectionalMap<String, Long> map: myChangeListMap.values()) {
      Long value = map.get(changeList.getName());
      if (value != null) {
        //noinspection HardCodedStringLiteral
        cellRenderer.append(" - Perforce #" + value.toString(), SimpleTextAttributes.GRAY_ATTRIBUTES);
        break;
      }
    }
  }

  public long getActiveChangeListNumber(final P4Connection connection) throws VcsException {
    return findOrCreatePerforceChangeList(connection, myChangeListManager.getDefaultChangeList());
  }

  @Nullable
  public Long getChangeListNumber(final P4Connection connection, final ChangeList changeList) {
    if (changeList.getName().equals(getDefaultChangeListName())) return -1L;
    final PerforceClient perforceClient = myAccuRevManager.getClient(connection);
    if (!isValidClient(perforceClient)) {
      LOG.debug("getChangeListNumber() returning null because description is not valid");
      return null;
    }
    final BidirectionalMap<String, Long> map = getChangeListMap(perforceClient);
    Long value = map.get(changeList.getName());
    if (value != null) return value.longValue();
    return null;
  }
}
